using System;
using System.IO;

namespace MonoTorrent.BEncoding
{
	/// <summary>
	/// Base interface for all BEncoded values.
	/// </summary>
	public abstract class BEncodedValue
	{
		/// <summary>
		/// Interface for all BEncoded values
		/// </summary>
		/// <param name="data">The byte array containing the BEncoded data</param>
		/// <returns></returns>
		public static BEncodedValue Decode(byte[] data)
		{
			if (data == null) throw new ArgumentNullException("data");

			using (var stream = new RawReader(new MemoryStream(data))) return (Decode(stream));
		}

		internal static BEncodedValue Decode(byte[] buffer, bool strictDecoding)
		{
			return Decode(buffer, 0, buffer.Length, strictDecoding);
		}

		/// <summary>
		/// Decode BEncoded data in the given byte array
		/// </summary>
		/// <param name="buffer">The byte array containing the BEncoded data</param>
		/// <param name="offset">The offset at which the data starts at</param>
		/// <param name="length">The number of bytes to be decoded</param>
		/// <returns>BEncodedValue containing the data that was in the byte[]</returns>
		public static BEncodedValue Decode(byte[] buffer, int offset, int length)
		{
			return Decode(buffer, offset, length, true);
		}

		public static BEncodedValue Decode(byte[] buffer, int offset, int length, bool strictDecoding)
		{
			if (buffer == null) throw new ArgumentNullException("buffer");

			if (offset < 0 || length < 0) throw new IndexOutOfRangeException("Neither offset or length can be less than zero");

			if (offset > buffer.Length - length) throw new ArgumentOutOfRangeException("length");

			using (var reader = new RawReader(new MemoryStream(buffer, offset, length), strictDecoding)) return (Decode(reader));
		}


		/// <summary>
		/// Decode BEncoded data in the given stream 
		/// </summary>
		/// <param name="stream">The stream containing the BEncoded data</param>
		/// <returns>BEncodedValue containing the data that was in the stream</returns>
		public static BEncodedValue Decode(Stream stream)
		{
			if (stream == null) throw new ArgumentNullException("stream");

			return Decode(new RawReader(stream));
		}


		/// <summary>
		/// Decode BEncoded data in the given RawReader
		/// </summary>
		/// <param name="reader">The RawReader containing the BEncoded data</param>
		/// <returns>BEncodedValue containing the data that was in the stream</returns>
		public static BEncodedValue Decode(RawReader reader)
		{
			if (reader == null) throw new ArgumentNullException("reader");

			BEncodedValue data;
			switch ((char)reader.PeekChar())
			{
				case ('i'): // Integer
					data = new BEncodedNumber();
					break;

				case ('d'): // Dictionary
					data = new BEncodedDictionary();
					break;

				case ('l'): // List
					data = new BEncodedList();
					break;

				case ('1'): // String
				case ('2'):
				case ('3'):
				case ('4'):
				case ('5'):
				case ('6'):
				case ('7'):
				case ('8'):
				case ('9'):
				case ('0'):
					data = new BEncodedString();
					break;

				default:
					throw new BEncodingException("Could not find what value to decode");
			}

			data.DecodeInternal(reader);
			return data;
		}


		/// <summary>
		/// Interface for all BEncoded values
		/// </summary>
		/// <param name="data">The byte array containing the BEncoded data</param>
		/// <returns></returns>
		public static T Decode<T>(byte[] data) where T: BEncodedValue
		{
			return (T)Decode(data);
		}


		/// <summary>
		/// Decode BEncoded data in the given byte array
		/// </summary>
		/// <param name="buffer">The byte array containing the BEncoded data</param>
		/// <param name="offset">The offset at which the data starts at</param>
		/// <param name="length">The number of bytes to be decoded</param>
		/// <returns>BEncodedValue containing the data that was in the byte[]</returns>
		public static T Decode<T>(byte[] buffer, int offset, int length) where T: BEncodedValue
		{
			return Decode<T>(buffer, offset, length, true);
		}

		public static T Decode<T>(byte[] buffer, int offset, int length, bool strictDecoding) where T: BEncodedValue
		{
			return (T)Decode(buffer, offset, length, strictDecoding);
		}


		/// <summary>
		/// Decode BEncoded data in the given stream 
		/// </summary>
		/// <param name="stream">The stream containing the BEncoded data</param>
		/// <returns>BEncodedValue containing the data that was in the stream</returns>
		public static T Decode<T>(Stream stream) where T: BEncodedValue
		{
			return (T)Decode(stream);
		}


		public static T Decode<T>(RawReader reader) where T: BEncodedValue
		{
			return (T)Decode(reader);
		}

		internal abstract void DecodeInternal(RawReader reader);

		/// <summary>
		/// Encodes the BEncodedValue into a byte array
		/// </summary>
		/// <returns>Byte array containing the BEncoded Data</returns>
		public byte[] Encode()
		{
			var buffer = new byte[LengthInBytes()];
			if (Encode(buffer, 0) != buffer.Length) throw new BEncodingException("Error encoding the data");

			return buffer;
		}


		/// <summary>
		/// Encodes the BEncodedValue into the supplied buffer
		/// </summary>
		/// <param name="buffer">The buffer to encode the information to</param>
		/// <param name="offset">The offset in the buffer to start writing the data</param>
		/// <returns></returns>
		public abstract int Encode(byte[] buffer, int offset);


		/// <summary>
		/// Returns the size of the byte[] needed to encode this BEncodedValue
		/// </summary>
		/// <returns></returns>
		public abstract int LengthInBytes();
	}
}